﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Objects;
using System.Linq.Expressions;
using System.Data.Objects.DataClasses;
using System.Reflection;
using System.Data.Common;
using System.Data;


namespace EFMassOperations
{
	/// <summary>
	/// Enables mass data manipulation operations for Entity Framework
	/// </summary>
	public static class EFMassOperations
	{
		static readonly string wrapExtentName = "WrapExtentForEF";

		/// <summary>
		/// Performs INSERT into <paramref name="destination"/>
		/// ObjectSet&lt;<typeparamref name="TObj"/>&gt; table from the specified
		/// SELECT <paramref name="query"/>.
		/// </summary>
		/// <typeparam name="TObj">Concrete EntityObject type to be inserted</typeparam>
		/// <typeparam name="TQuery">
		///		Type describing fields to be inserted into
		///		<paramref name="destination"/> table
		///	</typeparam>
		/// <param name="destination">
		///		Typed ObjectSet in wich data to be inserted
		///	</param>
		///	<param name="query">LINQ-to-entities query returning a data of type
		///		<typeparamref name="TQuery"/> to be inserted into <paramref name="destination"/>
		/// </param>
		/// <param name="propertyAccessor">
		///		An Expression (or lambda) taking <typeparamref name="TObj"/> as parameter
		///		and returning the same (possibly anonymous) type <typeparamref name="TQuery"/>
		///		as <paramref name="query"/> does. This expression sets a mapping between
		///		inserted EntityObject's properties and properties of selected type.
		///	</param>
		/// <param name="commandTimeout">
		///		Command timeout in seconds. If not set, ObjectContext.CommandTimeout is used
		///	</param>
		/// <returns>Count of database table rows affected</returns>
		/// <remarks>
		///		<para>Produces an SQL statement of the following form:</para>
		///		<para>INSERT INTO <paramref name="destination"/> (<paramref name="propertyAccessor"/>)
		///		SELECT (<paramref name="query"/>) AS &quot;Wrap&quot;</para>
		/// </remarks>
		/// <example>Example:
		/// <code><![CDATA[
		/// using EFMassOperations;
		/// 
		/// myEntities.user.Insert(
		///		(from history in myEntities.userhistory
		///		 where history.operation == "INSERT"
		///		 && history.operationid > 506
		///		 select new
		///		 {
		///			history.displayname,
		///			history.username,
		///			loweredusername = history.username.ToLower(),
		///			history.foreignuserid,
		///			createdate = history.syncdate
		///		 }),
		///		 (insertedUser) => new
		///		 {
		///			insertedUser.displayname,
		///			insertedUser.username,
		///			insertedUser.loweredusername,
		///			insertedUser.foreignuserid,
		///			insertedUser.createdate,
		///		 });
		/// ]]></code>
		/// </example>
		public static int Insert<TObj, TQuery>
			(this ObjectSet<TObj> destination,
			IQueryable<TQuery> query,
			Expression<Func<TObj, TQuery>> propertyAccessor,
			int? commandTimeout = null)
			where TObj : EntityObject
			where TQuery : class
		{
			return InsertInto(query, destination, propertyAccessor, commandTimeout);
		}

		/// <summary>
		/// Performs <paramref name="query"/> results insertion into database table of
		/// ObjectSet&lt;<typeparamref name="TObj"/>&gt; in a single SQL statement.
		/// </summary>
		/// <typeparam name="TQuery">Type returned from the <paramref name="query"/></typeparam>
		/// <typeparam name="TObj">Inserted EntityObject type</typeparam>
		/// <param name="query">LINQ-to-entities query for select</param>
		/// <param name="destination">Destination for insert</param>
		/// <param name="insertPropertyAccessor">
		///		An expression mapping from inserted to selected properties
		///	</param>
		/// <param name="commandTimeout">Command timeout in seconds</param>
		/// <returns>Count of database records affected</returns>
		/// <seealso cref="EFMassOperations.Insert"/>
		public static int InsertInto<TQuery, TObj>
			(this IQueryable<TQuery> query,
			ObjectSet<TObj> destination,
			Expression<Func<TObj, TQuery>> insertPropertyAccessor,
			int? commandTimeout = null)
			where TObj : EntityObject
			where TQuery : class
		{
			if (insertPropertyAccessor == null)
				throw new ArgumentNullException("insertPropertyAccessor");
			if (query == null)
				throw new ArgumentNullException("fieldsQuery");
			if (destination == null)
				throw new ArgumentNullException("destination");

			ObjectQuery<TQuery> fQuery = query as ObjectQuery<TQuery>;
			if (fQuery == null)
				throw new ArgumentException("InsertInto is usable only with ObjectQuery");
			if (fQuery.Context != destination.Context)
				throw new ArgumentException("fieldsQuery context and destination ObjectSet context should be the same");

			StringBuilder insertCommandBuilder = new StringBuilder();

			string tableName = EFMetadataUtils.GetTableName(destination);
			string selectCommandText = fQuery.ToTraceString();

			Dictionary<string, string> insertedPropsMap =
				GetInsertedObjectSetColumnMappings(destination, insertPropertyAccessor);
			Dictionary<string, string> selectedPropsMap = GetQueryColumnMappings(fQuery);

			insertCommandBuilder.Append("INSERT INTO ");
			insertCommandBuilder.Append(tableName);
			insertCommandBuilder.Append(" (");
			bool first = true;
			foreach (string propertyName in insertedPropsMap.Keys)
			{
				if (!first)
					insertCommandBuilder.Append(", ");
				string columnName = insertedPropsMap[propertyName];
				insertCommandBuilder.Append(columnName);
				first = false;
			}
			insertCommandBuilder.Append(") ");
			insertCommandBuilder.Append("SELECT ");
			DbCommandBuilder quoter = EFDbProviderFactoryUtils.GetSqlCommandBuilderCache(destination.Context);
			string wrap = quoter.QuoteIdentifier(wrapExtentName);
			first = true;
			foreach (string propertyName in selectedPropsMap.Keys)
			{
				if (!first)
					insertCommandBuilder.Append(", ");
				string columnName = selectedPropsMap[propertyName];
				insertCommandBuilder.Append(wrap + '.' + columnName);
				first = false;
			}
			insertCommandBuilder.Append(" FROM ( ");
			insertCommandBuilder.Append(selectCommandText);
			insertCommandBuilder.Append(" ) AS " + wrap);

			string insertText = insertCommandBuilder.ToString();

			int res;
			res = ExecuteCommand(destination.Context, fQuery.Parameters,
				insertText, commandTimeout);

			return res;
		}

		/// <summary>
		/// Performs UPDATE of <paramref name="destination"/>
		/// ObjectSet&lt;<typeparamref name="TObj"/>&gt; table FROM the specified
		/// SELECT <paramref name="query"/>.
		/// </summary>
		/// <typeparam name="TObj">Concrete EntityObject type of updated records</typeparam>
		/// <typeparam name="TQuery">Resulting type from the <paramref name="query"/></typeparam>
		/// <param name="destination">Typed ObjectSet for the table with data to be updated</param>
		/// <param name="query">
		///		The LINQ-to-enitities query returning a data of type <typeparamref name="TQuery"/>
		///		with properties used as new data for update and in <paramref name="wherePredicate"/>.
		///		Used in FROM sql expression.
		///	</param>
		/// <param name="setExpression">
		///		An expression returning <typeparamref name="TObj"/> object with
		///		properties needing to be updated set from <typeparamref name="TQuery"/> object.
		///		Provides columns assignment in SET sql expression.
		/// </param>
		/// <param name="wherePredicate">
		///		An expression to establish join between <paramref name="destination"/> updating
		///		table and <paramref name="query"/>.
		///		Used as WHERE sql predicate.
		/// </param>
		/// <param name="commandTimeout">
		///		Command timeout in seconds. If not set, ObjectContext.CommandTimeout is used
		/// </param>
		/// <returns>Count of database records affected</returns>
		/// <remarks>
		///		<para>Produces an SQL statement of the following form:</para>
		///		<para>UPDATE <paramref name="destination"/> SET <paramref name="setExpression"/>
		///		FROM (<paramref name="query"/>) AS &quot;Wrap&quot;
		///		WHERE <paramref name="wherePredicate"/></para>
		/// </remarks>
		/// <example>Example:
		/// <code><![CDATA[
		///	myEntities.product.Update(
		///		(from discount in myEntities.productDiscount
		///		 join prod in myEntities.product on discount.productId == prod.productId
		///		 select new
		///		 {
		///			discount.productId,
		///			newPrice = prod.price * (100.0 - discount.percent) / 100.0
		///		 },
		///		 (updateInfo) => new product
		///		 {
		///			product.releasePrice = updateInfo.newPrice
		///		 },
		///		 (prod, update) => prod.productId == update.productId);
		/// ]]></code>
		/// </example>
		public static int Update<TObj, TQuery>
			(this ObjectSet<TObj> destination,
			IQueryable<TQuery> query,
			Expression<Func<TQuery, TObj>> setExpression,
			Expression<Func<TObj, TQuery, bool>> wherePredicate,
			int? commandTimeout = null)
			where TObj : EntityObject
			where TQuery : class
		{
			if (destination == null)
				throw new ArgumentNullException("destination");
			if (query == null)
				throw new ArgumentNullException("fromQuery");
			if (setExpression == null)
				throw new ArgumentNullException("setExpression");
			if (wherePredicate == null)
				throw new ArgumentNullException("wherePredicate");

			ObjectQuery<TQuery> fQuery = query as ObjectQuery<TQuery>;
			if (fQuery == null)
				throw new ArgumentException("Update is usable only with ObjectQuery");
			if (fQuery.Context != destination.Context)
				throw new ArgumentException("fromQuery context and destination ObjectSet context should be the same");

			string tableName = EFMetadataUtils.GetTableName(destination);
			string selectCommandText = fQuery.ToTraceString();

			Dictionary<string, string> updateMap = GetObjectSetColumnMappings(destination);
			Dictionary<string, string> selectMap = GetQueryColumnMappings(fQuery);

			StringBuilder updateTextBuilder = new StringBuilder();
			updateTextBuilder.Append("UPDATE ");
			updateTextBuilder.Append(tableName);
			updateTextBuilder.Append(" SET ");

			Tuple<string, string>[] assignments = ParseAssignmentExpression(setExpression);

			DbCommandBuilder quoter = EFDbProviderFactoryUtils.GetSqlCommandBuilderCache(destination.Context);
			string wrap = quoter.QuoteIdentifier(wrapExtentName);

			bool first = true;
			foreach (Tuple<string, string> assignment in assignments)
			{
				if (!first)
					updateTextBuilder.Append(", ");
				string assignedColumn = updateMap[assignment.Item1];
				string valueColumn = selectMap[assignment.Item2];
				updateTextBuilder.Append(assignedColumn);
				updateTextBuilder.Append('=');
				updateTextBuilder.Append(wrap + '.' + valueColumn);
				first = false;
			}

			updateTextBuilder.Append(" FROM ( ");
			updateTextBuilder.Append(selectCommandText);
			updateTextBuilder.Append(" ) AS ");
			updateTextBuilder.Append(wrap);

			updateTextBuilder.Append(" WHERE ");

			ParseWhereExpression(wherePredicate, updateTextBuilder,
				(objPropName) => tableName + '.' + updateMap[objPropName],
				(queryPropName) => wrap + '.' + selectMap[queryPropName]);

			string updateText = updateTextBuilder.ToString();

			//Console.WriteLine(updateText);

			int res;
			res = ExecuteCommand(destination.Context, fQuery.Parameters,
				updateText, commandTimeout);

			return res;
		}

		/// <summary>
		/// Deletes all records from <paramref name="destination"/>
		/// ObjectSet&lt;<typeparamref name="T"/>&gt; table.
		/// </summary>
		/// <typeparam name="T">
		///		Concrete EntityObject type of records to be deleted
		///	</typeparam>
		/// <param name="destination">Typed ObjectSet for the table with data to be deleted</param>
		/// <param name="commandTimeout">
		///		Command timeout in seconds. If not set, ObjectContext.CommandTimeout is used
		/// </param>
		/// <returns>Count of database records affected</returns>
		/// <remarks>
		///		<para>Produces an SQL statement of the following form:</para>
		///		<para>DELETE FROM <paramref name="destination"/></para>
		/// </remarks>
		/// <example>Example:
		/// <code><![CDATA[
		/// myEntities.user.DeleteAll();
		/// ]]></code>
		/// </example>
		public static int DeleteAll<T>(this ObjectSet<T> destination,
			int? commandTimeout = null)
			where T : EntityObject
		{
			if (destination == null)
				throw new ArgumentNullException("destination");

			string tableName = EFMetadataUtils.GetTableName(destination);
			StringBuilder deleteTextBuilder = new StringBuilder();
			deleteTextBuilder.Append("DELETE FROM ");
			deleteTextBuilder.Append(tableName);
			string deleteText = deleteTextBuilder.ToString();
			int res;
			res = ExecuteCommand(destination.Context, null, deleteText, commandTimeout);
			return res;
		}

		/// <summary>
		/// Deletes records from <paramref name="destination"/>
		/// ObjectSet&lt;<typeparamref name="TObj"/>&gt; table where exists
		/// <paramref name="query"/> result with additional <paramref name="wherePredicate"/>.
		/// </summary>
		/// <typeparam name="TObj">Concrete EntityObject type of records to be deleted</typeparam>
		/// <typeparam name="TQuery">Type returned from the <paramref name="query"/></typeparam>
		/// <param name="destination">
		///		Typed ObjectSet for the table with data to be deleted
		/// </param>
		/// <param name="query">
		///		LINQ-to-entity query limiting together with <paramref name="wherePredicate"/>
		///		set of records to be deleted.
		///	</param>
		/// <param name="wherePredicate">
		///		A predicate expression (or lambda) comparing <typeparamref name="TObj"/>
		///		and <typeparamref name="TQuery"/> objects to filter records to be deleted.
		/// </param>
		/// <param name="commandTimeout">
		///		Command timeout in seconds. If not set, ObjectContext.CommandTimeout is used
		/// </param>
		/// <returns>Count of database records affected</returns>
		/// <remarks>
		///		<para>Produces an SQL statement of the following form:</para>
		///		<para>DELETE FROM <paramref name="destination"/> WHERE EXISTS (SELECT 0
		///		FROM (<paramref name="query"/>) AS &quot;Wrap&quot;
		///		WHERE <paramref name="wherePredicate"/>)</para>
		/// </remarks>
		/// <example>Example:
		/// <code><![CDATA[
		/// myEntities.user.DeleteWhereExists(
		///		(from history in myEntities.userhistory
		///		 where history.operation == "DELETE"
		///		 select history),
		///		 (usr, selected) => usr.foreignuserid == selected.foreignuserid);
		/// ]]></code>
		/// </example>
		public static int DeleteWhereExists<TObj, TQuery>(
			this ObjectSet<TObj> destination,
			IQueryable<TQuery> query,
			Expression<Func<TObj, TQuery, bool>> wherePredicate,
			int? commandTimeout = null)
			where TObj : EntityObject
			where TQuery : class
		{
			return DeleteWhereExistsImpl(destination, true, query, wherePredicate, commandTimeout);
		}

		/// <summary>
		/// Deletes records from <paramref name="destination"/>
		/// ObjectSet&lt;<typeparamref name="TObj"/>&gt; table where does not exist
		/// <paramref name="query"/> result with additional <paramref name="wherePredicate"/>.
		/// </summary>
		/// <typeparam name="TObj">Concrete EntityObject type of records to be deleted</typeparam>
		/// <typeparam name="TQuery">Type returned from the <paramref name="query"/></typeparam>
		/// <param name="destination">
		///		Typed ObjectSet for the table with data to be deleted
		/// </param>
		/// <param name="query">
		///		LINQ-to-entity query limiting together with <paramref name="wherePredicate"/>
		///		set of records to be deleted.
		///	</param>
		/// <param name="wherePredicate">
		///		A predicate expression (or lambda) comparing <typeparamref name="TObj"/>
		///		and <typeparamref name="TQuery"/> objects to filter records not to be deleted.
		/// </param>
		/// <param name="commandTimeout">
		///		Command timeout in seconds. If not set, ObjectContext.CommandTimeout is used
		/// </param>
		/// <returns>Count of database records affected</returns>
		/// <remarks>
		///		<para>Produces an SQL statement of the following form:</para>
		///		<para>DELETE FROM <paramref name="destination"/> WHERE NOT EXISTS (SELECT 0
		///		FROM (<paramref name="query"/>) AS &quot;Wrap&quot;
		///		WHERE <paramref name="wherePredicate"/>)</para>
		/// </remarks>
		/// <example>Example:
		/// <code><![CDATA[
		/// myEntities.user.DeleteWhereNotExists(
		///		(from history in myEntities.userhistory
		///		 where history.operation == "INSERT" || history.operation == "UPDATE"
		///		 select history),
		///		 (usr, selected) => usr.foreignuserid == selected.foreignuserid);
		/// ]]></code>
		/// </example>
		public static int DeleteWhereNotExists<TObj, TQuery>(
			this ObjectSet<TObj> destination,
			IQueryable<TQuery> query,
			Expression<Func<TObj, TQuery, bool>> wherePredicate,
			int? commandTimeout = null)
			where TObj : EntityObject
			where TQuery : class
		{
			return DeleteWhereExistsImpl(destination, false, query, wherePredicate, commandTimeout);
		}

		static int DeleteWhereExistsImpl<TObj, TQuery>(
			this ObjectSet<TObj> destination,
			bool exists,
			IQueryable<TQuery> query,
			Expression<Func<TObj, TQuery, bool>> wherePredicate,
			int? commandTimeout = null)
			where TObj : EntityObject
			where TQuery : class
		{
			if (destination == null)
				throw new ArgumentNullException("destination");
			if (query == null)
				throw new ArgumentNullException("query");
			if (wherePredicate == null)
				throw new ArgumentNullException("wherePredicate");

			ObjectQuery<TQuery> fQuery = (ObjectQuery<TQuery>)query;
			if (fQuery == null)
				throw new ArgumentException("Update is usable only with ObjectQuery");
			if (fQuery.Context != destination.Context)
				throw new ArgumentException("query context and destination ObjectSet context should be the same");

			string tableName = EFMetadataUtils.GetTableName(destination);
			string selectCommandText = fQuery.ToTraceString();

			Dictionary<string, string> deleteMap = GetObjectSetColumnMappings(destination);
			Dictionary<string, string> selectMap = GetQueryColumnMappings(fQuery);

			DbCommandBuilder quoter = EFDbProviderFactoryUtils.GetSqlCommandBuilderCache(destination.Context);
			string wrap = quoter.QuoteIdentifier(wrapExtentName);

			StringBuilder deleteTextBuilder = new StringBuilder();
			deleteTextBuilder.Append("DELETE FROM ");
			deleteTextBuilder.Append(tableName);
			deleteTextBuilder.Append(" WHERE ");
			if (!exists)
				deleteTextBuilder.Append("NOT ");
			deleteTextBuilder.Append("EXISTS (");
			deleteTextBuilder.Append("SELECT 0 FROM ( ");
			deleteTextBuilder.Append(selectCommandText);
			deleteTextBuilder.Append(" ) AS ");
			deleteTextBuilder.Append(wrap);
			deleteTextBuilder.Append(" WHERE ");
			ParseWhereExpression(wherePredicate, deleteTextBuilder,
				(objPropName) => tableName + '.' + deleteMap[objPropName],
				(queryPropName) => wrap + '.' + selectMap[queryPropName]);
			deleteTextBuilder.Append(")");
			string deleteText = deleteTextBuilder.ToString();

			int res;
			res = ExecuteCommand(destination.Context, fQuery.Parameters, deleteText, commandTimeout);

			return res;
		}

		static Dictionary<string, string> GetInsertedObjectSetColumnMappings<T, TInserted>
			(ObjectSet<T> set, Expression<Func<T, TInserted>> insertPropertyAccessor)
			where T : EntityObject
		{
			MemberInfo[] insertedProperties = CollectInsertedProperties(insertPropertyAccessor);
			return GetObjectSetColumnMappingsForProperties(set, insertedProperties);
		}

		static Dictionary<string, string> GetObjectSetColumnMappings<T>(ObjectSet<T> set)
			where T : EntityObject
		{
			Type type = typeof(T);
			Type scalarTypeAttribute = typeof(EdmScalarPropertyAttribute);
			IEnumerable<MemberInfo> scalarProperties =
				type.GetProperties().Where((p) => p.GetCustomAttributes(scalarTypeAttribute, false).Length != 0);
			return GetObjectSetColumnMappingsForProperties(set, scalarProperties);
		}

		static Dictionary<string, string> GetObjectSetColumnMappingsForProperties<T>
			(ObjectSet<T> set, IEnumerable<MemberInfo> forProperties)
			where T : EntityObject
		{
			IEnumerable<string> propertyNames = forProperties.Select((prop) => prop.Name);
			IEnumerable<string> columnNames = EFMetadataUtils.GetColumnNames(set, propertyNames);
			Dictionary<string, string> mappings = propertyNames.Zip(columnNames,
				(prop, col) => new KeyValuePair<string, string>(prop, col))
					.ToDictionary((kvp1) => kvp1.Key, (kvp2) => kvp2.Value);
			return mappings;
		}

		static MemberInfo[] CollectInsertedProperties<TObj, TRes>(Expression<Func<TObj, TRes>> propertyAccessor)
		{
			MemberInfo[] properties;
			NewExpression newExpression = propertyAccessor.Body as NewExpression;
			if (newExpression != null)
			{
				var arguments = newExpression.Arguments;
				properties = arguments.Select(delegate(Expression expression)
				{
					MemberExpression memberExpression = expression as MemberExpression;
					if (memberExpression == null)
						throw new ArgumentException();
					return memberExpression.Member;
				}).ToArray();
			}
			// TODO: expand with other useful different types of expressions
			else
				throw new ArgumentException();
			return properties;
		}

		static void CopyParameters(ObjectParameterCollection parameters, DbCommand command)
		{
			foreach (ObjectParameter objectParameter in parameters)
			{
				DbParameter cmdParameter = command.CreateParameter();				
				cmdParameter.ParameterName = objectParameter.Name;
				DbType dbType;
				if (DbParameterHelper.TryGetDbType(objectParameter.ParameterType, out dbType))
				{
					cmdParameter.DbType = dbType;
				}
				DbParameterHelper.SetParameterValue(cmdParameter, objectParameter.Value, true);
				command.Parameters.Add(cmdParameter);
			}
		}

		static TRes DoInConnection<TRes>(ObjectContext context, DbCommand command,
			Func<DbCommand, TRes> action)
		{
			DbConnection dbConnection = EFDbProviderFactoryUtils.GetDbConnection(context);
			if (dbConnection.State == System.Data.ConnectionState.Closed)
			{
				dbConnection.Open();
				try
				{
					return action(command);
				}
				finally
				{
					dbConnection.Close();
				}
			}
			else if (dbConnection.State == System.Data.ConnectionState.Open)
				return action(command);
			else
				throw new InvalidOperationException(string.Format(
					"Unexpected database connection state {0}", dbConnection.State));
		}

		static int ExecuteCommand(ObjectContext context, ObjectParameterCollection parameters,
			string commandText, int? commandTimeout)
		{
			int res;
			using (DbCommand command = EFDbProviderFactoryUtils.CreateCommand(context))
			{
				command.CommandText = commandText;
				if (commandTimeout.HasValue)
					command.CommandTimeout = commandTimeout.Value;
				else if (context.CommandTimeout.HasValue)
					command.CommandTimeout = context.CommandTimeout.Value;

				if (parameters != null)
					CopyParameters(parameters, command);

				res = DoInConnection(context, command, (cmd) => cmd.ExecuteNonQuery());
			}
			return res;
		}

		static Dictionary<string, string> GetQueryColumnMappings<T>(ObjectQuery<T> query)
		{
			// it is all based on assumption that mappings of column names of limited query
			// will not change compared to mappings of unlimited query :(
			DbCommandBuilder quoter = EFDbProviderFactoryUtils.GetSqlCommandBuilderCache(query.Context);
			ObjectQuery<T> limitedQuery;
			string[] columnNames = GetResultingColumnNames(query, out limitedQuery);
			Tuple<string, int>[] propertyMappings = EFQueryUtils.GetPropertyPositions(limitedQuery);
			int n = propertyMappings.Length;
			Dictionary<string, string> propColMappings = new Dictionary<string, string>(n);
			for (int i = 0; i < n; ++i)
				propColMappings.Add(propertyMappings[i].Item1,
					quoter.QuoteIdentifier(columnNames[propertyMappings[i].Item2]));
			return propColMappings;
		}

		static string[] GetResultingColumnNames<T>(ObjectQuery<T> query,
			out ObjectQuery<T> limitedQuery) // yup, expensive! another round-trip to db
		{
			IQueryable<T> queryable = (IQueryable<T>)query;
			queryable = queryable.Take(0);
			limitedQuery = ((ObjectQuery<T>)queryable);
			string text = limitedQuery.ToTraceString();
			string[] columnNames;

			using (DbCommand dummyCommand = EFDbProviderFactoryUtils.CreateCommand(query.Context))
			{
				dummyCommand.CommandText = text;
				CopyParameters(limitedQuery.Parameters, dummyCommand);
				columnNames = DoInConnection(query.Context, dummyCommand,
					delegate(DbCommand cmd)
					{
						string[] result;
						using (DbDataReader reader = cmd.ExecuteReader())
						{
							int n = reader.FieldCount;
							result = new string[n];
							for (int i = 0; i < n; ++i)
								result[i] = reader.GetName(i);
						}
						return result;
					});
			}
			return columnNames;
		}

		static Tuple<string, string>[] ParseAssignmentExpression<TQuery, TObj>(
			Expression<Func<TQuery, TObj>> expression)
		{
			MemberInitExpression memberInit = expression.Body as MemberInitExpression;
			if (memberInit == null)
				throw new ArgumentException();
			var bindings = memberInit.Bindings;
			int n = bindings.Count;
			Tuple<string, string>[] assignments = new Tuple<string, string>[n];
			for (int i = 0; i < n; ++i)
			{
				MemberAssignment assignment = bindings[i] as MemberAssignment;
				if (assignment == null)
					throw new ArgumentException();
				string assignedProperty = assignment.Member.Name;
				MemberExpression right = assignment.Expression as MemberExpression;
				if (right == null)
					throw new ArgumentException();
				string valueProperty = right.Member.Name;
				assignments[i] = new Tuple<string, string>(assignedProperty, valueProperty);
			}
			return assignments;
		}

		static void ParseWhereExpression<TObj, TQuery>(
			Expression<Func<TObj, TQuery, bool>> expression,
			StringBuilder result,
			Func<string, string> objPropToColumn,
			Func<string, string> queryPropToColumn)
		{
			BinaryExpression body = expression.Body as BinaryExpression;
			string tObjParamName = expression.Parameters[0].Name;
			string tQueryParamName = expression.Parameters[1].Name;
			ParsePredicateExpressionTree(body, result, objPropToColumn, queryPropToColumn,
				tObjParamName, tQueryParamName);
			return;
		}

		static void ParsePredicateExpressionTree(
			Expression expression,
			StringBuilder result,
			Func<string, string> objPropToColumn,
			Func<string, string> queryPropToColumn,
			string tObjParamName,
			string tQueryParamName)
		{
			BinaryExpression binaryExpression = expression as BinaryExpression;
			UnaryExpression unaryExpression = expression as UnaryExpression;
			MemberExpression memberExpression = expression as MemberExpression;
			if (binaryExpression != null)
			{
				result.Append('(');
				ParsePredicateExpressionTree(binaryExpression.Left, result,
					objPropToColumn, queryPropToColumn, tObjParamName, tQueryParamName);
				switch (binaryExpression.NodeType)
				{
					case ExpressionType.Equal:
						result.Append(" = ");
						break;
					case ExpressionType.LessThan:
						result.Append(" < ");
						break;
					case ExpressionType.LessThanOrEqual:
						result.Append(" > ");
						break;
					case ExpressionType.GreaterThan:
						result.Append(" > ");
						break;
					case ExpressionType.GreaterThanOrEqual:
						result.Append(" >= ");
						break;
					case ExpressionType.NotEqual:
						result.Append(" <> ");
						break;
					case ExpressionType.AndAlso:
						result.Append(" AND ");
						break;
					case ExpressionType.OrElse:
						result.Append(" OR ");
						break;
					default:
						throw new ArgumentException();
				}
				ParsePredicateExpressionTree(binaryExpression.Right, result,
					objPropToColumn, queryPropToColumn, tObjParamName, tQueryParamName);
				result.Append(')');
			}
			else if (unaryExpression != null)
			{
				if (unaryExpression.NodeType != ExpressionType.Not)
					throw new ArgumentException();
				result.Append("( NOT ");
				ParsePredicateExpressionTree(unaryExpression.Operand, result,
					objPropToColumn, queryPropToColumn, tObjParamName, tQueryParamName);
				result.Append(')');
			}
			else if (memberExpression != null)
			{
				string property = memberExpression.Member.Name;
				ParameterExpression param = memberExpression.Expression as ParameterExpression;
				if (param == null)
					throw new ArgumentException();
				string column;
				if (param.Name == tObjParamName)
					column = objPropToColumn(property);
				else if (param.Name == tQueryParamName)
					column = queryPropToColumn(property);
				else
					throw new ArgumentException();
				result.Append(column);
			}
		}
	}
}
